<?php

class Adept_Template_Parser_Tpl
{

    /**
     * @var Adept_Template_IObserver
     */
    protected $observer;

    protected $location;
    
    protected $tags;
    protected $textBlocks;
    /**
     * @var Adept_Template_Parser_Tpl_Rules
     */
    protected $rules;

    public function __construct()
    {
        $this->location = new Adept_Template_Location();
        $this->rules = new Adept_Template_Parser_Tpl_Rules();
    }
    
    /**
     * @return Adept_Template_Expression_Factory
     */
    protected function getExpressionFactory()
    {
    	return Adept_Template_Expression_Factory::getInstance();
    }

    public function getObserver()
    {
        return $this->observer;
    }

    public function setObserver($observer)
    {
        $this->observer = $observer;
    }

    protected function _quoteDelimiters($matches)
    {
        $s = $matches[1];
        $result = '';
        $replace = array('{' => '{ld /}', '}' => '{rd /}');
        for ($i = 0; $i < strlen($s); $i++) {
            $result .= isset($replace[$s[$i]]) ? $replace[$s[$i]] : $s[$i];
        }
        return $result;
    }

    protected function prepateSource($source)
    {
        $ld = preg_quote('{', '~');
        $rd = preg_quote('}', '~');

        // Replace { and } into {literal}...{/literal} to {ld /} or {rd /}
        $source = preg_replace_callback("/{$ld}literal\s*{$rd}(.*){$ld}\/literal\s*{$rd}/Uis",
        array($this, '_quoteDelimiters'), $source);
        // remove all {literal} and {/literal} tag pairs
        $source = preg_replace("/({$ld}[\/]?literal{$rd})/", '', $source);
        // remove comments
        $source = preg_replace("/({$ld}\*(?:[^\*]*)\*{$rd})/is", '', $source);
        return $source;
    }

    protected function parseTemplateTags($source)
    {
        $ld = preg_quote('{', '~');
        $rd = preg_quote('}', '~');

        preg_match_all("/{$ld}\s*(.*?)\s*{$rd}/s", $source, $_match);

        $this->tags = $_match[1];
        $this->textBlocks = preg_split("/{$ld}.*?{$rd}/s", $source);

    }
    
    protected function parseExpression($expression)
    {
        $rule = $this->rules->getExpressionRule();
    	if (preg_match("~^{$rule}$~", $expression)) {
    	    try {
    	       $object = Adept_Template_Expression_Factory::getInstance()->createExpression($expression);
    	    } catch (Adept_Exception $e) {
    	        throw new Adept_Template_Exception($e->getMessage(), $e->getParams(), $this->getCurrentLocation());
    	    }
    	    $this->observer->expression($this, $object);
    	    return true;
    	}
    	return false;
    }
    
    protected function trimQuotes($source)
    {
        if (strlen($source) > 1) {
            if (($source[0] == "'" && $source[strlen($source) - 1] == "'")
                || ($source[0] == '"' && $source[strlen($source) - 1] == '"')) {
                $source = substr($source, 1, strlen($source) - 2);
            }
        }
    	return $source;
    }

    protected function parseAttributes($string)
    {
        $attributeRule = $this->rules->getTagAttributeRule(true);
        $constValueRule = $this->rules->getConstValueRule();
        
        $attributes = array();
        if (preg_match_all("~{$attributeRule}~s", $string, $match)) {
            
            for ($i = 0; $i < count($match[1]); $i++) {
                $name = $match[1][$i];
                $value = $match[2][$i];                
                
                if (preg_match("~^{$constValueRule}$~", $value)) {
                    
                    $attributes[$name] = new Adept_Template_Attribute_Constant($this->trimQuotes($value));
                } else {
                    try {
                    $attributes[$name] = new Adept_Template_Attribute_Expression(
                        $this->getExpressionFactory()->createExpression($value));
                    } catch (Adept_Exception $e) {
                        throw new Adept_Template_Exception($e->getMessage(), $e->getParams(), 
                            $this->getCurrentLocation());
                    }
                }
            }
        }
        return $attributes;
    }
    
    protected function parseTag($string)
    {
        $rule = $this->rules->getTagRule(true);
        if (preg_match("~^{$rule}$~", $string, $match)) {
            
            $name = $match[2];
            
            if ($match[1] == '/') {
                $this->observer->endElement($this, $name);                
                return true;
            }
            $closed = $match[4] == '/';
            $this->observer->startElement($this, $name, $this->parseAttributes($match[3]), $closed);
            return true;
        }
        return false;
    }
    
    public function parseTextBlock($string) 
    {
    	if (strlen($string) > 0) {
    	    $this->observer->characters($this, $string);
    	}
    	return true;
    }	
    
    public function parseProcessingInstruction($string) 
    {
    	$rule = $this->rules->getProcessingInstructionRule();
        if (preg_match("~^{$rule}$~", $string, $match)) {
            
            $this->observer->processingInstruction($this, $match[1], $match[2]);
            return true;
        }
        return false;
    }	
    
    protected function increaseLineNumber($inc)
    {
    	$this->location->setLineNumber($this->location->getLineNumber() + $inc);
    }
    
    public function parse($source)
    {
        $this->location->setLineNumber(1);
        
        $source = $this->prepateSource($source);
        $this->parseTemplateTags($source);

        for ($i = 0; $i < count($this->tags); $i++) {
            
            $this->parseTextBlock($this->textBlocks[$i]);
            
            $this->increaseLineNumber(substr_count($this->textBlocks[$i], "\n"));
            
            $currentTag = $this->tags[$i];
            switch (true) {
                case $this->parseExpression($currentTag):
                    break;
                case $this->parseTag($currentTag):
                    break;
                case $this->parseProcessingInstruction($currentTag):
                    break;
                default:
                    throw new Adept_Template_Exception("Invalid entity", array(), $this->getCurrentLocation());
            }
            $this->increaseLineNumber(substr_count($currentTag, "\n"));
        }
        if (isset($this->textBlocks[count($this->tags)])) {
            $this->parseTextBlock($this->textBlocks[count($this->tags)]);
        }
    }

    public function getTemplateName() 
    {
        return $this->location->getFileName();
    }
    
    public function setTemplateName($templateName) 
    {
        $this->location->setFileName($templateName);
    }
    
    public function getLocation() 
    {
    	return $this->location;
    }	
    
    public function getCurrentLocation() 
    {
    	return clone $this->location;
    }	
    
}
